/* eslint-disable no-unused-vars */
/* eslint-disable no-new */
/* eslint-disable no-undef */
class Vue {
  constructor (options) {
    this.$options = options || {}
    this.data = this.$options.data || {}
    this.computed = this.$options.computed || {}
    this.methods = this.$options.methods || {}
    // 1.1 proxy data's propperty: vm.data.xxx -> vm.xxx
    Object.keys(this.data).forEach((key) => {
      this.proxyKeys(key)
    })
    // 1.2 计算属性
    Object.keys(this.computed).forEach((key) => {
      this.proxyKeys(key)
    })
    // 1.3 proxy data's methods: vm.methds.xxx -> vm.xxx
    Object.keys(this.methods).forEach((key) => {
      this.proxyKeys(key)
    })
    // 2.1 definReactive: 劫持所有属性，通知发布者属性的变化，发布者通知所有dependence(Dep)
    observer(this.data)
    // 2.2 监听computed
    observer(this.computed, this)
    // 3. compile 编译节点，所有依赖（watcher）的收集在此步骤进行
    new Compile(options.el, this)
    // 4. mounted: 编译初始化完毕，包含所有节点的文档碎片已成功挂载到根节点，vue实例化完毕
    // 代码健壮性处理
    typeof options.mounted === 'function' && options.mounted.call(this)
  }
  proxyKeys (key) {
    Object.defineProperty(this, key, {
      enumerable: true,
      configurable: false,
      get: function () {
        return this.data[key] !== undefined
          ? this.data[key] : this.computed[key] !== undefined
            ? this.computed[key] : this.methods[key]
      },
      set: function (newVal) {
        this.data[key] !== undefined
          ? this.data[key] = newVal : this.computed[key] !== undefined
            ? this.computed[key] = newVal : this.methods[key] = newVal
      }
    })
  }
}

/* eslint-disable no-undef */
/* eslint-disable no-new */

class Compile {
  constructor (el, vm) {
    this.vm = vm
    this.el = document.querySelector(el)
    this.fragment = null
    this.init()
  }
  init () {
    if (this.el) {
      this.fragment = this.nodeToFragment(this.el)
      this.compileElement(this.fragment)
      this.el.appendChild(this.fragment)
    } else {
      throw new Error('element not found,Check and try again')
    }
  }
  nodeToFragment (el) {
    let fragment = document.createDocumentFragment()
    let child = el.firstChild
    while (child) {
      // 将Dom元素移入fragment中操作再挂载到DOM树，避免引起过多次的重绘和回流
      fragment.appendChild(child)
      child = el.firstChild
    }
    return fragment
  }
  compileElement (el) {
    let childNodes = el.childNodes
    Array.prototype.slice.call(childNodes).forEach((node) => {
      let reg = /\{\{\s*(.*?)\s*\}\}/
      let text = node.textContent

      if (this.isElementNode(node)) {
        this.compileAttrs(node)
      } else if (this.isTextNode(node) && reg.test(text)) {
        this.compileText(node, reg.exec(text)[1])
      }

      if (node.childNodes && node.childNodes.length) {
        this.compileElement(node)
      }
    })
  }
  compileAttrs (node) {
    let nodeAttrs = node.attributes
    Array.prototype.slice.call(nodeAttrs).forEach((attr) => {
      let attrName = attr.name
      if (this.isDirective(attrName)) {
        let exp = attr.value
        let dir = attrName.substring(2)
        if (this.isEventDirective(dir)) { // 事件指令
          this.compileEvent(node, this.vm, exp, dir)
        } else { // v-model 指令
          this.compileModel(node, this.vm, exp)
        }
        node.removeAttribute(attrName)
      } else if (attrName.startsWith('@')) {
        let exp = attr.value
        let dir = attrName
        this.compileEvent(node, this.vm, exp, dir)
        node.removeAttribute(attrName)
      }
    })
  }
  compileText (node, exp) {
    let initText = null
    initText = this.vm[exp]
    this.updateText(node, initText, node.textContent)
    new Watcher(this.vm, exp, (value, nodeContent) => {
      this.updateText(node, value, nodeContent)
    })
  }
  compileEvent (node, vm, exp, dir) {
    let eventType = ''
    if (dir.indexOf(':') !== -1) {
      eventType = dir.split(':')[1]
    } else {
      eventType = dir.substring(1)
    }
    let cb = vm.methods && vm.methods[exp]
    if (eventType && cb) {
      node.addEventListener(eventType, cb.bind(vm), false)
    }
  }
  compileModel (node, vm, exp) {
    let val = vm[exp]
    this.modelUpdater(node, val)
    new Watcher(vm, exp, (value) => {
      this.modelUpdater(node, value)
    })

    node.addEventListener('input', (e) => {
      let newValue = e.target.value
      if (val === newValue) {
        return
      }
      this.vm[exp] = newValue
      val = newValue
    })
  }
  updateText (node, value, nodeContent) {
    // 修复不能精确定位到插值括号内部变量问题
    Watcher.nodeContent = node.textContent
    if (typeof value === 'undefined') {
      node.textContent = ''
    } else {
      if (nodeContent) node.textContent = nodeContent.replace(/\{\{\s*(.*?)\s*\}\}/, value)
      else node.textContent = value
    }
  }
  modelUpdater (node, value) {
    node.value = typeof value === 'undefined' ? '' : value
  }
  isDirective (attr) {
    return attr.indexOf('v-') === 0
  }
  isEventDirective (dir) {
    return dir.indexOf('on:') === 0
  }
  isElementNode (node) {
    return node.nodeType === 1
  }
  isTextNode (node) {
    return node.nodeType === 3
  }
}

/* eslint-disable no-undef */
// eslint-disable-next-line no-unused-vars

class Watcher {
  constructor (vm, exp, cb) {
    this.cb = cb
    this.vm = vm
    this.exp = exp
    if (!this.nodeContent) {
      this.nodeContent = Watcher.nodeContent
      Watcher.nodeContent = null
    }
    this.value = this.get() // 将自己添加到订阅器的操作
  }
  update () {
    this.run()
  }
  run () {
    let value = null
    value = this.vm[this.exp]
    this.value = value
    this.cb(value, this.nodeContent)
  }
  get () {
    Dep.target = this // 暂存自己
    let value
    value = this.vm[this.exp] // 执行监听器里的get函数保存watcher
    Dep.target = null // 释放自己
    return value
  }
}

/* eslint-disable no-unused-vars */

class Observer {
  constructor (data, vm) {
    this.vm = vm
    this.data = data
    this.walk(data)
  }
  walk (data) {
    Object.keys(data).forEach((key) => {
      if (typeof data[key] !== 'function') {
        this.defineReactive(data, key, data[key])
      } else {
        this.defineReactive(data, key, data[key].bind(this.vm))
      }
    })
  }
  defineReactive (data, key, val) {
    let dep = new Dep()
    if (typeof data[key] !== 'function') {
      Object.defineProperty(data, key, {
        enumerable: true,
        configurable: false,
        get: function () {
          if (Dep.target) {
            dep.addSub(Dep.target)
          }
          return val
        },
        set: function (newVal) {
          if (newVal === val) {
            return false
          }
          val = newVal
          dep.notify()
        }
      })
    } else {
      Object.defineProperty(data, key, {
        enumerable: true,
        configurable: false,
        get: function () {
          return val()
        },
        set: function () {
          throw new Error('computed property no setter')
        }
      })
    }
  }
}

class Dep {
  constructor () {
    this.subs = []
  }
  addSub (sub) {
    this.subs.push(sub)
  }
  notify () {
    this.subs.forEach((sub) => {
      sub.update()
    })
  }
}

function observer (data, vm) {
  if (!data || typeof data !== 'object') {
    return false
  }
  return new Observer(data, vm)
}
